const fs = require('fs');
const path = require('path');
const showdown = require('showdown');
converter = new showdown.Converter();
const { BASE_URL, MARKDOWN_ROOT, DATA_ROOT, RUNTIME_ROOT } = require('../config/config');
const CategoryData = require('../config/category');
const MenuData = require('../config/menu');
// const sqlite3 = require('sqlite3').verbose();
// const db = new sqlite3.Database(':memory:');
// const db = new sqlite3.Database(RUNTIME_ROOT + 'database.sqlite3');
const { Sequelize, DataTypes, Op } = require('sequelize');

const sequelize = new Sequelize({
    dialect: 'sqlite',
    storage: RUNTIME_ROOT + 'database.sqlite'
});

(async () => {
    try {
        await sequelize.authenticate();
        console.log('Connection has been established successfully.');
    } catch (error) {
        console.error('Unable to connect to the database:', error);
    }
})()

const Article = sequelize.define('Article', {
    id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    },
    title: DataTypes.STRING,
    category1: DataTypes.STRING,
    category2: DataTypes.STRING,
    tag: DataTypes.STRING,
    created_at: DataTypes.STRING,
    summary: DataTypes.TEXT,
}, {
    tableName: 'tbl_article'
});


/**
 * 模型类
 * @method getCategoryData() 返回分类数据
 * @method getTagData() 返回标签数据
 * @method getListForNew() 返回最新文章列表
 * @method getListForRecommend() 返回推荐文章列表
 * @method getListForCategory() 返回分类文章列表
 * @method getDetailData() 返回文章详情数据
 * @method getPagination() 列表分页通用方法
 * @method updateDb() 更新全局数据
 * @method log() 记录日志
 * @method parseMarkdownContent() 解析 Markdown 内容
 * @method getCategoryLinkInfo() 返回文章的分类链接信息
 * @method getTagLinkInfo() 返回文章的标签链接信息
 * @method getBreadcrumbHtml() 返回页面的面包屑导航信息
 * @method paging() 返回分页后的列表数据和分页条
 */
class Model {
    /**
     * 忽略的目录
     */
    skipDirs = ['public', 'static', 'images', 'image'];

    /**
     * 更新数据库
     * 功能：重建数据表，重新读取所有文件并填充到数据表。
     */
    async updateDb() {
        let categories = [...CategoryData];
        // 重新创建数据表
        await sequelize.drop();
        await sequelize.sync({ force: true });

        // 写入文章数据
        let dirPath = '', list = [], listAll = [];
        for await (let item1 of categories) {
            dirPath = MARKDOWN_ROOT + item1.name + '/';
            if (!fs.existsSync(dirPath)) {
                console.log('[错误] Model.updateDb() 一级分类目录不存在：', dirPath);
                continue;
            }
            list = await this.readFiles(item1.name);
            listAll = listAll.concat(list);
            await this._fillArticleDb(list);
            item1.fileCount = list.length;
            list = [];
            if (item1.children) {
                for await (let item2 of item1.children) {
                    dirPath = MARKDOWN_ROOT + item1.name + '/' + item2.namkime + '/';
                    if (!fs.existsSync(dirPath)) {
                        console.log('[错误] Model.updateDb() 二级分类目录不存在：', dirPath);
                        continue;
                    }
                    list = await this.readFiles(item1.name, item2.name);
                    listAll = listAll.concat(list);
                    await this._fillArticleDb(list);
                    item2.fileCount = list.length;
                    item1.fileCount += item2.fileCount;
                    list = [];
                }
            }
        }
        fs.writeFileSync(RUNTIME_ROOT + '_category.json', JSON.stringify(categories));

        // 更新标签数据
        let tags = {};
        listAll.filter(v => v.tag).forEach(item => {
            item.tag.split(' ').forEach(str => {
                if (!str || !str.trim()) {
                    return;
                }
                if (!tags[str]) {
                    tags[str] = 0;
                }
                tags[str]++;
            })
        })

        list = [];
        for (let str in tags) {
            list.push({
                title: str, count: tags[str]
            });
        }
        fs.writeFileSync(RUNTIME_ROOT + '_tags.json', JSON.stringify(list));

        // 读取所有文章，生成搜索引擎静态数据源
        listAll.sort((a, b) => {
            return b.created_ts - a.created_ts;
        })
        fs.writeFileSync(DATA_ROOT + 'search.js', "function getData() {\n\treturn " + JSON.stringify(listAll.map(({ title, url, summary, created_at }) => ({ title, url, summary, created_at }))) + ";\n}\n");
    }

    /**
     * 填充文章数据到数据库
     * @param {Array} list 
     */
    async _fillArticleDb(list) {
        for await (let item of list) {
            if (item.tag) {
                item.tag += ' ';
            }
            await Article.create(item);
        }
    }

    /**
     * 记录日志
     */
    log(message) {
        fs.writeFileSync(RUNTIME_ROOT + 'log.txt', new Date().toISOString() + "\n" + message + "\n");
    }

    /**
     * 返回合并的完整路径 
     */
    joinPath(path1, path2, ...morePath) {
        return [path1, path2, ...morePath].filter(v => v && v.toString().trim().length > 0).map(v => v.toString().trim()).join('/');
    }

    /**
     * 返回指定路径的文章详情
     */
    getDetailData(title, category1, category2) {
        let mdFile = MARKDOWN_ROOT;
        if (category1) {
            mdFile += category1 + '/';
        }
        if (category2) {
            mdFile += category2 + '/';
        }
        mdFile += title + '.md';
        let attr = this.parseMarkdownContent(mdFile, true);
        return this.stdRecord(attr, {
            title,
            category1,
            category2,
        }, true);
    }

    /**
     * 返回指定目录下的所有文章列表
     */
    async readFiles(category1, category2 = '') {
        let categoryPath = this.joinPath(category1, category2) + '/';
        let fromDir = MARKDOWN_ROOT + categoryPath;
        const dir = await fs.promises.opendir(fromDir);
        let list = [], listRecommend = [], item = null, currentName = '', currentPath = '', extname = '', basename = '', attr = null, tags = [];
        for await (const dirent of dir) {
            currentName = dirent.name;
            if (currentName.startsWith('_')) {
                // 跳过下划线开头的文件或目录
                continue;
            }
            if (dirent.isDirectory()) {
                // 跳过目录（只读取当前目录下的所有文件，无需递归子目录）
                continue;
            }
            extname = path.extname(currentName);
            if (!extname || extname.toLowerCase() != '.md') {
                // 只读取后缀名是 .md 的文件
                continue;
            }
            basename = path.basename(currentName, extname);
            if (basename.toLowerCase() == 'readme') {
                // 跳过自述文件 README.md
                continue;
            }
            currentPath = fromDir + currentName;
            attr = this.parseMarkdownContent(currentPath);
            // console.log('[调试] attr', attr);
            item = this.stdRecord(attr, {
                title: basename,
                category1,
                category2,
            });
            if (basename.startsWith('推荐 -')) {
                listRecommend.push(item);
            } else {
                list.push(item);
            }
        }
        list.sort((a, b) => {
            return b.created_ts - a.created_ts;
        })
        listRecommend.sort((a, b) => {
            return b.created_ts - a.created_ts;
        })
        return listRecommend.concat(list);
    }

    /**
     * 返回分类数据
     */
    getCategoryData() {
        return JSON.parse(fs.readFileSync(RUNTIME_ROOT + '_category.json'));
    }

    /**
     * 返回标签数据
     */
    getTagData() {
        let filePath = RUNTIME_ROOT + '_tags.json';
        if (!fs.existsSync(filePath)) {
            return [];
        }
        return JSON.parse(fs.readFileSync(filePath));
    }

    /**
     * 返回菜单数据
     */
    getMenuData() {
        return MenuData;
    }

    /**
     * 返回指定分类的文章列表
     * @param {String} category1 一级分类名称
     * @param {String} category2 二级分类名称
     * @returns Array
     */
    async pagingListForCategory(category1, category2, pageId, pageSize = 10, urlTemplate = 'index-{pageId}.html') {
        let offset = (pageId - 1) * pageSize, limit = pageSize;
        let where = {};
        where.category1 = category1;
        if (category2) {
            where.category2 = category2;
        }
        const { count, rows } = await Article.findAndCountAll({
            where,
            order: [
                ['created_at', 'DESC']
            ],
            offset: offset,
            limit: limit
        });
        let list = [];
        for (let item of rows) {
            item.dataValues = this.stdRecord(item.dataValues, null, true);
            list.push(item.dataValues);
        }
        return { list, total: count, pageCount: Math.ceil(count / pageSize), pagination: this.getPagination(count, pageId, pageSize, urlTemplate) };
    }

    /**
     * 返回指定标签名称的文章列表
     * @param {String} tagName 标签名称
     * @returns 
     */
    async pagingListForTag(tagName, pageId, pageSize = 10, urlTemplate = 'index-{pageId}.html') {
        let offset = (pageId - 1) * pageSize, limit = pageSize;
        const { count, rows } = await Article.findAndCountAll({
            where: {
                tag: {
                    [Op.like]: `%${tagName} %`,
                },
            },
            order: [
                ['created_at', 'DESC']
            ],
            offset: offset,
            limit: limit
        });
        let list = rows.map(v => this.stdRecord(v.dataValues, null, true));
        return { list, total: count, pageCount: Math.ceil(count / pageSize), pagination: this.getPagination(count, pageId, pageSize, urlTemplate) };
    }

    /**
     * 返回最新文章列表
     */
    async pagingListForNew(pageId, pageSize = 10, urlTemplate = 'index-{pageId}.html') {
        let offset = (pageId - 1) * pageSize, limit = pageSize;
        const { count, rows } = await Article.findAndCountAll({
            order: [
                ['created_at', 'DESC']
            ],
            offset,
            limit
        });
        let list = [];
        for (let item of rows) {
            item.dataValues = this.stdRecord(item.dataValues, null, true);
            list.push(item.dataValues);
        }
        return { list, total: count, pageCount: Math.ceil(count / pageSize), pagination: this.getPagination(count, pageId, pageSize, urlTemplate) };
    }

    /**
     * 返回推荐文章列表
     */
    async pagingListForRecommend(pageId, pageSize = 10, urlTemplate = 'index-{pageId}.html') {
        let offset = (pageId - 1) * pageSize, limit = pageSize;
        const { count, rows } = await Article.findAndCountAll({
            where: {
                title: {
                    [Op.like]: '推荐 -%',
                }
            },
            order: [
                ['created_at', 'DESC']
            ],
            offset: offset,
            limit: limit
        });
        let list = [];
        for (let item of rows) {
            item.dataValues = this.stdRecord(item.dataValues, null, true);
            list.push(item.dataValues);
        }
        return { list, total: count, pageCount: Math.ceil(count / pageSize), pagination: this.getPagination(count, pageId, pageSize, urlTemplate) };
    }

    /**
     * 返回标准格式的记录数据
     */
    stdRecord(data, data2 = {}, withMoreAttr = false) {
        data = Object.assign(Object.assign({
            title: '', // 标题
            summary: '', // 摘要
            category1: '', // 分类1
            category2: '', // 分类2
            tag: '', // 标签
            paramfile:'', // 参数文件（JSON 格式）
            created_at: '', // 创建时间
            updated_at: '', // 更新时间
        }, data), data2);

        if (withMoreAttr) {
            let list = [];
            if (data.tag) {
                data.tag.split(' ').forEach(text => {
                    if (text.trim().length > 0) {
                        list.push({ text, url: BASE_URL + '/' + 'tag/' + text + '.html' });
                    }
                })
            }
            data.tagLinks = list;
            list = [];
            if (data.category1) {
                list.push({ text: data.category1, url: BASE_URL + '/' + data.category1 + '/index.html' });
            }
            if (data.category2) {
                list.push({ text: data.category2, url: BASE_URL + '/' + data.category1 + '/' + data.category2 + '/index.html' });
            }
            data.categoryLinks = list;
            data.url = BASE_URL + '/' + this.joinPath(data.category1, data.category2) + '/' + data.title + '.html';
        }

        return data;
    }

    /**
     * 解析 Markdown 文件内容
     * @param {String} mdFile 文件路径
     * @param {Boolean} keepContent 是否保留原文主体内容（比较占内存），默认 false。
     * @param {Boolean} keepContent 是否保留HTML主体内容（比较占内存），默认 false。
     * @returns 
     */
    parseMarkdownContent(mdFile, keepHtml = false, keepContent = false) {
        let data = {
            title: '',
            summary: '',
            tag: '',
            paramfile: '',
            path: mdFile,
            url: BASE_URL + '/' + mdFile.substring(MARKDOWN_ROOT.length).replace('.md', '.html'),
            created_at: '',
            created_ts: 0,
            content: '',
            error_code: 0,
        }
        if (!fs.existsSync(mdFile)) {
            data.error_code = 404;
            return data;
        }
        let content = fs.readFileSync(mdFile).toString().trim();
        let match = null;
        // 解析 yaml 元数据
        if (content.startsWith('---')) {
            content = content.substring(3).trim();
            let metaStr = content.substring(0, content.indexOf('---')).trim();
            match = metaStr.match(/\bdate: \"([^\"]+)\"/);
            data.created_at = match ? match[1] : '';
            match = metaStr.match(/\btitle: \"([^\"]+)\"/);
            data.title = match ? match[1].replace(/\s+/g, ' ').trim() : '';
            match = metaStr.match(/\btag: \"([^\"]+)\"/);
            data.tag = match ? match[1].replace(/\s+/g, ' ').trim() : '';
            match = metaStr.match(/\bsummary: \"([^\"]+)\"/);
            data.summary = match ? match[1] : '';
            match = metaStr.match(/\bparamfile: \"([^\"]+)\"/);
            data.paramfile = match ? match[1] : '';
            content = content.substring(content.indexOf('---') + 3).trim();
        }
        if (!data.title) {
            match = content.match(/\B# .+/);
            data.title = match ? match[1] : '';
        }
        if (!data.title) {
            data.title = path.basename(mdFile, path.extname(mdFile));
        }
        // 去掉1号标题
        content = content.replace(/\B# .+/, '');
        // 替换图片路径
        let pattern = new RegExp("../../../public/", "ig");
        content = content.replace(pattern, '../../');
        pattern = new RegExp("../../public/", "ig");
        content = content.replace(pattern, '../');
        data.content = content;
        data.htmlContent = converter.makeHtml(content);
        content = null;
        // summary
        if (!data.summary) {
            data.summary = data.htmlContent.replace(/<[\/]*[^>]+>/ig, '').slice(0, 400) + ' ...';
        }
        if (!data.created_at) {
            data.created_at = '2021-10-01'
        }
        // 去掉主体内容，节省内存消耗
        if (!keepContent) {
            data.content = null;
        }
        if (!keepHtml) {
            data.htmlContent = null;
        }
        data.created_ts = Math.floor(new Date(data.created_at).getTime() / 1000);
        return data;
    }

    getCategoryLinkInfo(category1, category2 = '') {
        let data = [];
        if (category1) {
            data.push({ text: category1, url: category1 + '/index.html' });
        }
        if (category2) {
            data.push({ text: category2, url: category1 + '/' + category2 + '/index.html' });
        }
        return data;
    }

    getTagLinkInfo(tagStr) {
        let data = [];
        if (!tagStr) {
            return data;
        }
        tagStr.split(' ').forEach(text => {
            data.push({ text, url: text + '/index.html' });
        })
        return data;
    }

    getBreadcrumbHtml(category1 = '', category2 = '', appendNodes = []) {
        let html = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
        html += '<li class="breadcrumb-item"><a href="' + BASE_URL + '/">首页</a></li>';
        if (category1) {
            html += '<li class="breadcrumb-item"><a href="' + BASE_URL + '/' + category1 + '/index.html">' + category1 + '</a></li>';
        }
        if (category2) {
            html += '<li class="breadcrumb-item"><a href="' + BASE_URL + '/' + category2 + '/index.html">' + category2 + '</a></li>';
        }
        if (appendNodes.length > 0) {
            appendNodes.forEach(item => {
                html += '<li class="breadcrumb-item"><a href="' + BASE_URL + '/' + item.url + '">' + item.text + '</a></li>';
            })
        }
        html += '</ol></nav>';
        return html;
    }

    /**
     * 列表分页，返回分页后的列表数据和分页条
     */
    paging(list, pageId = 1, pageSize = 20, urlTemplate = 'index-{pageId}.html') {
        if (!pageId || pageId < 1) {
            pageId = 1;
        }
        let start = (pageId - 1) * pageSize, end = pageId * pageSize, total = list.length;
        return { list: list.slice(start, end), pagination: this.getPagination(total, pageId, pageSize, urlTemplate) };
    }

    /**
     * 返回分页信息
     * < 1 2 3 4 5 ... 100 >
     * < 1 ...6 7 8 9 10... 100 >
     * < 1 ... 96 97 98 99 100 >
     */
    getPagination(total, pageId = 1, pageSize = 20, urlTemplate = 'index-{pageId}.html') {
        if (!pageId || pageId < 1) {
            pageId = 1;
        }
        let pageCount = Math.ceil(total / pageSize), len = 5, half = Math.floor(len / 2);
        let prevId = pageId > 1 ? (pageId - 1) : 1, nextId = pageId < pageCount ? (pageId + 1) : pageCount;
        let url = '', ellipsisButton = '<li class="page-item disabled"><a class="page-link">...</a></li>';
        let html = '<nav><ul class="pagination">';
        url = urlTemplate.replace('{pageId}', prevId);
        html += '<li class="page-item"><a class="page-link" href="' + url + '">&laquo;</a></li>';
        url = urlTemplate.replace('{pageId}', 1);
        html += '<li class="page-item"><a class="page-link' + (pageId == 1 ? ' active' : '') + '" href="' + url + '">1</a></li>';
        if (pageCount <= len) {
            // < 1 2 3 4 5 >
            for (let i = 2; i < pageCount; i++) {
                url = urlTemplate.replace('{pageId}', i);
                html += '<li class="page-item"><a class="page-link' + (pageId == i ? ' active' : '') + '" href="' + url + '">' + i + '</a></li>'
            }
        } else {
            if (pageId <= half) {
                // < 1 2 3 4 5 ... 100 >
                for (let i = 2; i <= len; i++) {
                    url = urlTemplate.replace('{pageId}', i);
                    html += '<li class="page-item"><a class="page-link' + (pageId == i ? ' active' : '') + '" href="' + url + '">' + i + '</a></li>'
                }
                html += ellipsisButton;
            } else if (pageId >= (pageCount - half - 1)) {
                // < 1 ... 96 97 98 99 100 >
                html += ellipsisButton;
                for (let i = (pageCount - len); i < pageCount; i++) {
                    url = urlTemplate.replace('{pageId}', i);
                    html += '<li class="page-item"><a class="page-link' + (pageId == i ? ' active' : '') + '" href="' + url + '">' + i + '</a></li>'
                }
            } else {
                // < 1 ...6 7 8 9 10... 100 >
                html += ellipsisButton;
                for (let i = (pageId - half + 1); i <= (pageId + half); i++) {
                    url = urlTemplate.replace('{pageId}', i);
                    html += '<li class="page-item"><a class="page-link' + (pageId == i ? ' active' : '') + '" href="' + url + '">' + i + '</a></li>'
                }
                html += ellipsisButton;
            }
        }
        if (pageCount > 1) {
            url = urlTemplate.replace('{pageId}', pageCount);
            html += '<li class="page-item"><a class="page-link' + (pageId == pageCount ? ' active' : '') + '" href="' + url + '">' + pageCount + '</a></li>';
        }
        url = urlTemplate.replace('{pageId}', nextId);
        html += '<li class="page-item"><a class="page-link" href="' + url + '">&raquo;</a></li>';
        html += '</ul></nav>';
        return html;
    }
}

module.exports = Model;
